#include <userver/utils/from_string.hpp>

#include <limits>
#include <random>
#include <type_traits>

#include <fmt/core.h>
#include <fmt/format.h>
#include <gtest/gtest.h>

#include <userver/compiler/demangle.hpp>

USERVER_NAMESPACE_BEGIN

namespace {

// std::to_string is unusable, because it does not keep the full precision of
// floating-point types
template <typename T>
std::string ToString(T value) {
    if constexpr (sizeof(value) == 1) {
        // Prevent printing int8_t and uint8_t as a character
        return std::to_string(static_cast<int>(value));
    } else if constexpr (std::is_same_v<T, long double> && FMT_VERSION < 100100) {
        // fmt before 10.1.0 formatted long double incorrectly
        // https://github.com/fmtlib/fmt/issues/3564
        return fmt::format("{:.{}g}", value, std::numeric_limits<long double>::max_digits10);
    } else {
        return fmt::to_string(value);
    }
}

template <typename T>
auto TestInvalid(const std::string& input) {
    ASSERT_THROW(utils::FromString<T>(input), std::runtime_error)
        << "type = " << compiler::GetTypeName<T>() << ", input = \"" << input << "\"";
}

template <typename StringType, typename T>
auto CheckConverts(StringType input, T expected_result) {
    T actual_result{};
    ASSERT_NO_THROW(actual_result = utils::FromString<T>(input))
        << "type = " << compiler::GetTypeName<T>() << ", input = \"" << input << "\"";
    ASSERT_EQ(actual_result, expected_result)
        << "type = " << compiler::GetTypeName<T>() << ", input = \"" << input << "\"";
}

template <typename T>
auto TestConverts(const std::string& input, T expected_result) {
    CheckConverts(input.c_str(), expected_result);
    CheckConverts(input, expected_result);
    CheckConverts(std::string_view{input}, expected_result);
}

template <typename T>
auto TestPreserves(T value) {
    TestConverts(ToString(value), value);
}

template <typename T>
auto DistributionForTesting() {
    if constexpr (std::is_floating_point_v<T>) {
        return std::normal_distribution<T>();
    } else {
        // 8-bit types are not allowed in uniform_int_distribution, so increase the
        // T size.
        return std::uniform_int_distribution<std::common_type_t<T, unsigned short>>(
            std::numeric_limits<T>::min(), std::numeric_limits<T>::max()
        );
    }
}

}  // namespace

template <typename T>
class FromStringTest : public ::testing::Test {};

using NumericTypes = ::testing::
    Types<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, float, double, long double>;
TYPED_TEST_SUITE(FromStringTest, NumericTypes);

TYPED_TEST(FromStringTest, Sign) {
    using T = TypeParam;

    TestConverts("0", T{0});
    TestConverts("+0", T{0});
    TestConverts("-0", T{0});
    TestConverts("+1", T{1});

    if constexpr (std::is_signed_v<T>) {
        TestConverts("-1", T{-1});
    }
}

TYPED_TEST(FromStringTest, Randomized) {
    using T = TypeParam;
    constexpr int kTestIterations = 100;

    // `randomEngine` is initialized with a fixed default seed
    // NOLINTNEXTLINE(cert-msc51-cpp)
    std::default_random_engine random_engine;
    auto distribution = DistributionForTesting<T>();

    for (int i = 0; i < kTestIterations; ++i) {
        TestPreserves(static_cast<T>(distribution(random_engine)));
    }
}

TYPED_TEST(FromStringTest, SmallModuleFloating) {
    using T = TypeParam;

    if constexpr (std::is_floating_point_v<T>) {
        TestPreserves(std::numeric_limits<T>::denorm_min());
        TestInvalid<T>(ToString(std::numeric_limits<T>::denorm_min()) + "0");
    }
}

TYPED_TEST(FromStringTest, Limits) {
    using T = TypeParam;

    TestPreserves(std::numeric_limits<T>::min());
    TestPreserves(std::numeric_limits<T>::max());

    if constexpr (std::is_floating_point_v<T>) {
        TestPreserves(std::numeric_limits<T>::lowest());
        TestPreserves(-std::numeric_limits<T>::lowest());
        TestPreserves(std::numeric_limits<T>::infinity());
        TestPreserves(-std::numeric_limits<T>::infinity());
        ASSERT_TRUE(std::isnan(utils::FromString<T>("nan")));
    }
}

TYPED_TEST(FromStringTest, StrangeDecimalPoints) {
    using T = TypeParam;

    if constexpr (std::is_floating_point_v<T>) {
        TestConverts(".1", T{0.1L});
        TestConverts("1.", T{1.0});
        TestInvalid<T>("..");
    }
}

TYPED_TEST(FromStringTest, HasZeroByte) {
    using T = TypeParam;

    if constexpr (std::is_floating_point_v<T>) {
        TestInvalid<T>({"1.1\0 <- zero byte", 17});
    }

    TestInvalid<T>({"1\0 <- zero byte", 15});
}

TYPED_TEST(FromStringTest, Exponents) {
    using T = TypeParam;

    if constexpr (std::is_floating_point_v<T>) {
        TestConverts("0e0", T{0e0});
        TestConverts("12e3", T{12e3});
        TestConverts("-12e3", T{-12e3});
    }
}

TYPED_TEST(FromStringTest, NonDecimal) {
    using T = TypeParam;

    TestConverts("010", T{10});
    TestConverts("0000000000000000000000000000000010", T{10});

    TestInvalid<T>("0b10");
    TestInvalid<T>("0o10");

    TestInvalid<T>("0x10");
    TestInvalid<T>("0X10");
}

TYPED_TEST(FromStringTest, ExtraSpaces) {
    using T = TypeParam;

    TestInvalid<T>(" 1");
    TestInvalid<T>("1 ");
    TestInvalid<T>("1\t");
    TestInvalid<T>("1\n");
    TestInvalid<T>("1\r");
    TestInvalid<T>("1\v");
}

TYPED_TEST(FromStringTest, ExtraGarbage) {
    using T = TypeParam;

    TestInvalid<T>("#");
    TestInvalid<T>("#1");
    TestInvalid<T>("1#");
    TestInvalid<T>("1 #");
    TestInvalid<T>("1-");
    TestInvalid<T>("a1");
    TestInvalid<T>("1a");
    TestInvalid<T>("+-1");
    TestInvalid<T>("+-0");

    if constexpr (!std::is_floating_point_v<T>) {
        TestInvalid<T>("1.0");
        TestInvalid<T>("1.");
        TestInvalid<T>(".1");
    }
}

TYPED_TEST(FromStringTest, Overflow) {
    using T = TypeParam;

    if constexpr (std::is_unsigned_v<T>) {
        TestInvalid<T>("-1");
    } else {
        TestInvalid<T>(ToString(std::numeric_limits<T>::lowest()) + "0");
    }
    TestInvalid<T>(ToString(std::numeric_limits<T>::max()) + "0");

    if constexpr (std::is_floating_point_v<T>) {
        TestInvalid<T>("1e1000000000000000000000000");
    } else {
        TestInvalid<T>("1000000000000000000000000");
    }
}

TYPED_TEST(FromStringTest, ExceptionDetails) {
    using T = TypeParam;
    std::string what;

    try {
        utils::FromString<T>(".blah");
    } catch (const std::runtime_error& e) {
        what = e.what();
    } catch (const std::exception& e) {
        // swallow
    }

    ASSERT_FALSE(what.empty());
    ASSERT_NE(what.find(".blah"), std::string::npos);
    // NOTE: GetTypeName(typeid(T)) for old version of GCC
    ASSERT_NE(what.find(compiler::GetTypeName(typeid(T))), std::string::npos);
}

TEST(FromString, StringViewToFloatingPointSmall) {
    char buffer[5];
    for (int i = 1; i <= 5; i++) {
        buffer[i - 1] = '0' + i;
    }
    const std::string_view str(buffer, 4);
    const double expected = 1234;
    CheckConverts(str, expected);
}

TEST(FromString, StringViewToFloatingPointBig) {
    char buffer[33];
    std::fill(buffer, buffer + 33, '0');

    buffer[1] = '.';
    buffer[32] = '9';
    std::string_view str(buffer, 32);
    long double expected = 0;
    CheckConverts(str, expected);

    buffer[1] = '0';
    buffer[31] = '9';
    str = std::string_view{buffer, 32};
    expected = 9;
    CheckConverts(str, expected);
}

USERVER_NAMESPACE_END
